<!DOCTYPE html>
<html lang="zh-cn">
<head>
  <title>controller - 为企业级框架和应用而生</title>
  <meta charset="utf-8">
  <meta name="description" content="index.description">
  <meta http-equiv="X-UA-Compatible" content="IE=Edge,chrome=1">
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
  <link rel="icon" href="/images/favicon.png" type="image/x-icon">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/docsearch.js/2/docsearch.min.css" />
<link rel="stylesheet" href="/css/index.css">

    <script>
    !function(t,e,a,r,c){t.TracertCmdCache=t.TracertCmdCache||[],t[c]=window[c]||
      {_isInit:!0,call:function(){t.TracertCmdCache.push(arguments)},
      start:function(t){this.call('start',t)}},t[c].l=new Date;
      var n=e.createElement(a),s=e.getElementsByTagName(a)[0];
      n.async=!0,n.src=r,s.parentNode.insertBefore(n,s)}
    (window,document,'script','https://tracert.alipay.com/tracert.js','Tracert');
      Tracert.start({
        plugins: [ 'BucName' ],
        spmAPos: 'a454',
        spmBPos: 'b4893',
      });
    </script>
  
</head>
<body>
  <div class="nav" >
  <header>
    <a href="/zh-cn/" class="nav-logo leftpadding" alt="egg"><img src="https://zos.alipayobjects.com/rmsportal/VTcUYAaoKqXyHJbLAPyF.svg"></a>
    <ul class="nav-item">
      <li>
        <form id="search-form">
          <input type="text" id="search-query" class="search-query st-default-search-input">
        </form>
      </li>
      <li><a href="/zh-cn/intro/" alt="指南">指南</a></li><li><a href="/api/" alt="API">API</a></li><li><a href="/zh-cn/tutorials/index.html" alt="教程">教程</a></li><li><a href="https://github.com/search?q=topic%3Aegg-plugin&type=Repositories" alt="插件">插件</a></li><li><a href="https://github.com/eggjs/egg/releases" alt="发布日志">发布日志</a></li>
      
      
        <li class="translations">
          <a class="nav-link">Translations</a>
          <span class="arrow"></span><ul id="dropdownContent" class="dropdown-content"><li><a id="en" href="/en/basics/controller.html" >English</a></li><li><a id="zh-cn" href="/zh-cn/basics/controller.html" style="color: #22ab28">中文</a></li></ul>
        </li>
      
      <li><iframe src="https://ghbtns.com/github-btn.html?user=eggjs&repo=egg&type=star&count=true" frameborder="0" scrolling="0" width="150px" height="20px"></iframe></li>
    </ul>
    <a id="mobileTrigger" href="#" class="mobile-trigger">
      <ul>
        <li></li>
        <li></li>
        <li></li>
      </ul>
    </a>
  </header>
</div>
  <div id="container" class="container">
    <div class="page-main">
  <article class="markdown-body">
    <h1>controller</h1>
    <h2 id="什么是-controller"><a class="markdown-anchor" href="#什么是-controller">#</a> 什么是 Controller</h2>
<p><a href="./router.html">前面章节</a>写到，我们通过 Router 将用户的请求基于 method 和 URL 分发到了对应的 Controller 上，那 Controller 负责做什么？</p>
<p>简单的说 Controller 负责<strong>解析用户的输入，处理后返回相应的结果</strong>，例如</p>
<ul>
<li>在 <a href="https://en.wikipedia.org/wiki/Representational_state_transfer" target="_blank" rel="noopener">RESTful</a> 接口中，Controller 接受用户的参数，从数据库中查找内容返回给用户或者将用户的请求更新到数据库中。</li>
<li>在 HTML 页面请求中，Controller 根据用户访问不同的 URL，渲染不同的模板得到 HTML 返回给用户。</li>
<li>在代理服务器中，Controller 将用户的请求转发到其他服务器上，并将其他服务器的处理结果返回给用户。</li>
</ul>
<p>框架推荐 Controller 层主要对用户的请求参数进行处理（校验、转换），然后调用对应的 <a href="./service.html">service</a> 方法处理业务，得到业务结果后封装并返回：</p>
<ol>
<li>获取用户通过 HTTP 传递过来的请求参数。</li>
<li>校验、组装参数。</li>
<li>调用 Service 进行业务处理，必要时处理转换 Service 的返回结果，让它适应用户的需求。</li>
<li>通过 HTTP 将结果响应给用户。</li>
</ol>
<h2 id="如何编写-controller"><a class="markdown-anchor" href="#如何编写-controller">#</a> 如何编写 Controller</h2>
<p>所有的 Controller 文件都必须放在 <code>app/controller</code> 目录下，可以支持多级目录，访问的时候可以通过目录名级联访问。Controller 支持多种形式进行编写，可以根据不同的项目场景和开发习惯来选择。</p>
<h3 id="controller-类推荐"><a class="markdown-anchor" href="#controller-类推荐">#</a> Controller 类（推荐）</h3>
<p>我们可以通过定义 Controller 类的方式来编写代码：</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="comment">// app/controller/post.js</span></span><br><span class="line"><span class="keyword">const</span> Controller = <span class="built_in">require</span>(<span class="string">'egg'</span>).Controller;</span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">PostController</span> <span class="keyword">extends</span> <span class="title">Controller</span> </span>&#123;</span><br><span class="line">  <span class="keyword">async</span> create() &#123;</span><br><span class="line">    <span class="keyword">const</span> &#123; ctx, service &#125; = <span class="keyword">this</span>;</span><br><span class="line">    <span class="keyword">const</span> createRule = &#123;</span><br><span class="line">      title: &#123; <span class="attr">type</span>: <span class="string">'string'</span> &#125;,</span><br><span class="line">      content: &#123; <span class="attr">type</span>: <span class="string">'string'</span> &#125;,</span><br><span class="line">    &#125;;</span><br><span class="line">    <span class="comment">// 校验参数</span></span><br><span class="line">    ctx.validate(createRule);</span><br><span class="line">    <span class="comment">// 组装参数</span></span><br><span class="line">    <span class="keyword">const</span> author = ctx.session.userId;</span><br><span class="line">    <span class="keyword">const</span> req = <span class="built_in">Object</span>.assign(ctx.request.body, &#123; author &#125;);</span><br><span class="line">    <span class="comment">// 调用 Service 进行业务处理</span></span><br><span class="line">    <span class="keyword">const</span> res = <span class="keyword">await</span> service.post.create(req);</span><br><span class="line">    <span class="comment">// 设置响应内容和响应状态码</span></span><br><span class="line">    ctx.body = &#123; <span class="attr">id</span>: res.id &#125;;</span><br><span class="line">    ctx.status = <span class="number">201</span>;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"><span class="built_in">module</span>.exports = PostController;</span><br></pre></td></tr></table></figure>
<p>我们通过上面的代码定义了一个 <code>PostController</code> 的类，类里面的每一个方法都可以作为一个 Controller 在 Router 中引用到，我们可以从 <code>app.controller</code> 根据文件名和方法名定位到它。</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="comment">// app/router.js</span></span><br><span class="line"><span class="built_in">module</span>.exports = <span class="function"><span class="params">app</span> =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">const</span> &#123; router, controller &#125; = app;</span><br><span class="line">  router.post(<span class="string">'createPost'</span>, <span class="string">'/api/posts'</span>, controller.post.create);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>Controller 支持多级目录，例如如果我们将上面的 Controller 代码放到 <code>app/controller/sub/post.js</code> 中，则可以在 router 中这样使用：</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="comment">// app/router.js</span></span><br><span class="line"><span class="built_in">module</span>.exports = <span class="function"><span class="params">app</span> =&gt;</span> &#123;</span><br><span class="line">  app.router.post(<span class="string">'createPost'</span>, <span class="string">'/api/posts'</span>, app.controller.sub.post.create);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>定义的 Controller 类，会在每一个请求访问到 server 时实例化一个全新的对象，而项目中的 Controller 类继承于 <code>egg.Controller</code>，会有下面几个属性挂在 <code>this</code> 上。</p>
<ul>
<li><code>this.ctx</code>: 当前请求的上下文 <a href="./extend.html#context">Context</a> 对象的实例，通过它我们可以拿到框架封装好的处理当前请求的各种便捷属性和方法。</li>
<li><code>this.app</code>: 当前应用 <a href="./extend.html#application">Application</a> 对象的实例，通过它我们可以拿到框架提供的全局对象和方法。</li>
<li><code>this.service</code>：应用定义的 <a href="./service.html">Service</a>，通过它我们可以访问到抽象出的业务层，等价于 <code>this.ctx.service</code> 。</li>
<li><code>this.config</code>：应用运行时的<a href="./config.html">配置项</a>。</li>
<li><code>this.logger</code>：logger 对象，上面有四个方法（<code>debug</code>，<code>info</code>，<code>warn</code>，<code>error</code>），分别代表打印四个不同级别的日志，使用方法和效果与 <a href="../core/logger.html#context-logger">context logger</a> 中介绍的一样，但是通过这个 logger 对象记录的日志，在日志前面会加上打印该日志的文件路径，以便快速定位日志打印位置。</li>
</ul>
<h4 id="自定义-controller-基类"><a class="markdown-anchor" href="#自定义-controller-基类">#</a> 自定义 Controller 基类</h4>
<p>按照类的方式编写 Controller，不仅可以让我们更好的对 Controller 层代码进行抽象（例如将一些统一的处理抽象成一些私有方法），还可以通过自定义 Controller 基类的方式封装应用中常用的方法。</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="comment">// app/core/base_controller.js</span></span><br><span class="line"><span class="keyword">const</span> &#123; Controller &#125; = <span class="built_in">require</span>(<span class="string">'egg'</span>);</span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">BaseController</span> <span class="keyword">extends</span> <span class="title">Controller</span> </span>&#123;</span><br><span class="line">  get user() &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">this</span>.ctx.session.user;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  success(data) &#123;</span><br><span class="line">    <span class="keyword">this</span>.ctx.body = &#123;</span><br><span class="line">      success: <span class="literal">true</span>,</span><br><span class="line">      data,</span><br><span class="line">    &#125;;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  notFound(msg) &#123;</span><br><span class="line">    msg = msg || <span class="string">'not found'</span>;</span><br><span class="line">    <span class="keyword">this</span>.ctx.throw(<span class="number">404</span>, msg);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"><span class="built_in">module</span>.exports = BaseController;</span><br></pre></td></tr></table></figure>
<p>此时在编写应用的 Controller 时，可以继承 BaseController，直接使用基类上的方法：</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="comment">//app/controller/post.js</span></span><br><span class="line"><span class="keyword">const</span> Controller = <span class="built_in">require</span>(<span class="string">'../core/base_controller'</span>);</span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">PostController</span> <span class="keyword">extends</span> <span class="title">Controller</span> </span>&#123;</span><br><span class="line">  <span class="keyword">async</span> list() &#123;</span><br><span class="line">    <span class="keyword">const</span> posts = <span class="keyword">await</span> <span class="keyword">this</span>.service.listByUser(<span class="keyword">this</span>.user);</span><br><span class="line">    <span class="keyword">this</span>.success(posts);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<h3 id="controller-方法不推荐使用只是为了兼容"><a class="markdown-anchor" href="#controller-方法不推荐使用只是为了兼容">#</a> Controller 方法（不推荐使用，只是为了兼容）</h3>
<p>每一个 Controller 都是一个 async function，它的入参为请求的上下文 <a href="./extend.html#context">Context</a> 对象的实例，通过它我们可以拿到框架封装好的各种便捷属性和方法。</p>
<p>例如我们写一个对应到 <code>POST /api/posts</code> 接口的 Controller，我们会在 <code>app/controller</code> 目录下创建一个 <code>post.js</code> 文件</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="comment">// app/controller/post.js</span></span><br><span class="line">exports.create = <span class="keyword">async</span> ctx =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> createRule = &#123;</span><br><span class="line">    title: &#123; <span class="attr">type</span>: <span class="string">'string'</span> &#125;,</span><br><span class="line">    content: &#123; <span class="attr">type</span>: <span class="string">'string'</span> &#125;,</span><br><span class="line">  &#125;;</span><br><span class="line">  <span class="comment">// 校验参数</span></span><br><span class="line">  ctx.validate(createRule);</span><br><span class="line">  <span class="comment">// 组装参数</span></span><br><span class="line">  <span class="keyword">const</span> author = ctx.session.userId;</span><br><span class="line">  <span class="keyword">const</span> req = <span class="built_in">Object</span>.assign(ctx.request.body, &#123; author &#125;);</span><br><span class="line">  <span class="comment">// 调用 service 进行业务处理</span></span><br><span class="line">  <span class="keyword">const</span> res = <span class="keyword">await</span> ctx.service.post.create(req);</span><br><span class="line">  <span class="comment">// 设置响应内容和响应状态码</span></span><br><span class="line">  ctx.body = &#123; <span class="attr">id</span>: res.id &#125;;</span><br><span class="line">  ctx.status = <span class="number">201</span>;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>
<p>在上面的例子中我们引入了许多新的概念，但还是比较直观，容易理解的，我们会在下面对它们进行更详细的介绍。</p>
<h2 id="http-基础"><a class="markdown-anchor" href="#http-基础">#</a> HTTP 基础</h2>
<p>由于 Controller 基本上是业务开发中唯一和 HTTP 协议打交道的地方，在继续往下了解之前，我们首先简单的看一下 HTTP 协议是怎样的。</p>
<p>如果我们发起一个 HTTP 请求来访问前面例子中提到的 Controller：</p>
<figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">curl -X POST http://localhost:3000/api/posts --data &apos;&#123;&quot;title&quot;:&quot;controller&quot;, &quot;content&quot;: &quot;what is controller&quot;&#125;&apos; --header &apos;Content-Type:application/json; charset=UTF-8&apos;</span><br></pre></td></tr></table></figure>
<p>通过 curl 发出的 HTTP 请求的内容就会是下面这样的：</p>
<figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">POST /api/posts HTTP/1.1</span><br><span class="line">Host: localhost:3000</span><br><span class="line">Content-Type: application/json; charset=UTF-8</span><br><span class="line"></span><br><span class="line">&#123;&quot;title&quot;: &quot;controller&quot;, &quot;content&quot;: &quot;what is controller&quot;&#125;</span><br></pre></td></tr></table></figure>
<p>请求的第一行包含了三个信息，我们比较常用的是前面两个：</p>
<ul>
<li>method：这个请求中 method 的值是 <code>POST</code>。</li>
<li>path：值为 <code>/api/posts</code>，如果用户的请求中包含 query，也会在这里出现</li>
</ul>
<p>从第二行开始直到遇到的第一个空行位置，都是请求的 Headers 部分，这一部分中有许多常用的属性，包括这里看到的 Host，Content-Type，还有 <code>Cookie</code>，<code>User-Agent</code> 等等。在这个请求中有两个头：</p>
<ul>
<li><code>Host</code>：我们在浏览器发起请求的时候，域名会用来通过 DNS 解析找到服务的 IP 地址，但是浏览器也会将域名和端口号放在 Host 头中一并发送给服务端。</li>
<li><code>Content-Type</code>：当我们的请求有 body 的时候，都会有 Content-Type 来标明我们的请求体是什么格式的。</li>
</ul>
<p>之后的内容全部都是请求的 body，当请求是 POST, PUT, DELETE 等方法的时候，可以带上请求体，服务端会根据 Content-Type 来解析请求体。</p>
<p>在服务端处理完这个请求后，会发送一个 HTTP 响应给客户端</p>
<figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">HTTP/1.1 201 Created</span><br><span class="line">Content-Type: application/json; charset=utf-8</span><br><span class="line">Content-Length: 8</span><br><span class="line">Date: Mon, 09 Jan 2017 08:40:28 GMT</span><br><span class="line">Connection: keep-alive</span><br><span class="line"></span><br><span class="line">&#123;&quot;id&quot;: 1&#125;</span><br></pre></td></tr></table></figure>
<p>第一行中也包含了三段，其中我们常用的主要是<a href="https://en.wikipedia.org/wiki/List_of_HTTP_status_codes" target="_blank" rel="noopener">响应状态码</a>，这个例子中它的值是 201，它的含义是在服务端成功创建了一条资源。</p>
<p>和请求一样，从第二行开始到下一个空行之间都是响应头，这里的 Content-Type, Content-Length 表示这个响应的格式是 JSON，长度为 8 个字节。</p>
<p>最后剩下的部分就是这次响应真正的内容。</p>
<h2 id="获取-http-请求参数"><a class="markdown-anchor" href="#获取-http-请求参数">#</a> 获取 HTTP 请求参数</h2>
<p>从上面的 HTTP 请求示例中可以看到，有好多地方可以放用户的请求数据，框架通过在 Controller 上绑定的 Context 实例，提供了许多便捷方法和属性获取用户通过 HTTP 请求发送过来的参数。</p>
<h3 id="query"><a class="markdown-anchor" href="#query">#</a> query</h3>
<p>在 URL 中 <code>?</code> 后面的部分是一个 Query String，这一部分经常用于 GET 类型的请求中传递参数。例如 <code>GET /posts?category=egg&amp;language=node</code> 中 <code>category=egg&amp;language=node</code> 就是用户传递过来的参数。我们可以通过 <code>ctx.query</code> 拿到解析过后的这个参数体</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">PostController</span> <span class="keyword">extends</span> <span class="title">Controller</span> </span>&#123;</span><br><span class="line">  <span class="keyword">async</span> listPosts() &#123;</span><br><span class="line">    <span class="keyword">const</span> query = <span class="keyword">this</span>.ctx.query;</span><br><span class="line">    <span class="comment">// &#123;</span></span><br><span class="line">    <span class="comment">//   category: 'egg',</span></span><br><span class="line">    <span class="comment">//   language: 'node',</span></span><br><span class="line">    <span class="comment">// &#125;</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>当 Query String 中的 key 重复时，<code>ctx.query</code> 只取 key 第一次出现时的值，后面再出现的都会被忽略。<code>GET /posts?category=egg&amp;category=koa</code> 通过 <code>ctx.query</code> 拿到的值是 <code>{ category: 'egg' }</code>。</p>
<p>这样处理的原因是为了保持统一性，由于通常情况下我们都不会设计让用户传递 key 相同的 Query String，所以我们经常会写类似下面的代码：</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="keyword">const</span> key = ctx.query.key || <span class="string">''</span>;</span><br><span class="line"><span class="keyword">if</span> (key.startsWith(<span class="string">'egg'</span>)) &#123;</span><br><span class="line">  <span class="comment">// do something</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>而如果有人故意发起请求在 Query String 中带上重复的 key 来请求时就会引发系统异常。因此框架保证了从 <code>ctx.query</code> 上获取的参数一旦存在，一定是字符串类型。</p>
<h4 id="queries"><a class="markdown-anchor" href="#queries">#</a> queries</h4>
<p>有时候我们的系统会设计成让用户传递相同的 key，例如 <code>GET /posts?category=egg&amp;id=1&amp;id=2&amp;id=3</code>。针对此类情况，框架提供了 <code>ctx.queries</code> 对象，这个对象也解析了 Query String，但是它不会丢弃任何一个重复的数据，而是将他们都放到一个数组中：</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="comment">// GET /posts?category=egg&amp;id=1&amp;id=2&amp;id=3</span></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">PostController</span> <span class="keyword">extends</span> <span class="title">Controller</span> </span>&#123;</span><br><span class="line">  <span class="keyword">async</span> listPosts() &#123;</span><br><span class="line">    <span class="built_in">console</span>.log(<span class="keyword">this</span>.ctx.queries);</span><br><span class="line">    <span class="comment">// &#123;</span></span><br><span class="line">    <span class="comment">//   category: [ 'egg' ],</span></span><br><span class="line">    <span class="comment">//   id: [ '1', '2', '3' ],</span></span><br><span class="line">    <span class="comment">// &#125;</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p><code>ctx.queries</code> 上所有的 key 如果有值，也一定会是数组类型。</p>
<h3 id="router-params"><a class="markdown-anchor" href="#router-params">#</a> Router params</h3>
<p>在 <a href="./router.html">Router</a> 中，我们介绍了 Router 上也可以申明参数，这些参数都可以通过 <code>ctx.params</code> 获取到。</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="comment">// app.get('/projects/:projectId/app/:appId', 'app.listApp');</span></span><br><span class="line"><span class="comment">// GET /projects/1/app/2</span></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">AppController</span> <span class="keyword">extends</span> <span class="title">Controller</span> </span>&#123;</span><br><span class="line">  <span class="keyword">async</span> listApp() &#123;</span><br><span class="line">    assert.equal(<span class="keyword">this</span>.ctx.params.projectId, <span class="string">'1'</span>);</span><br><span class="line">    assert.equal(<span class="keyword">this</span>.ctx.params.appId, <span class="string">'2'</span>);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<h3 id="body"><a class="markdown-anchor" href="#body">#</a> body</h3>
<p>虽然我们可以通过 URL 传递参数，但是还是有诸多限制：</p>
<ul>
<li><a href="http://stackoverflow.com/questions/417142/what-is-the-maximum-length-of-a-url-in-different-browsers" target="_blank" rel="noopener">浏览器中会对 URL 的长度有所限制</a>，如果需要传递的参数过多就会无法传递。</li>
<li>服务端经常会将访问的完整 URL 记录到日志文件中，有一些敏感数据通过 URL 传递会不安全。</li>
</ul>
<p>在前面的 HTTP 请求报文示例中，我们看到在 header 之后还有一个 body 部分，我们通常会在这个部分传递 POST、PUT 和 DELETE 等方法的参数。一般请求中有 body 的时候，客户端（浏览器）会同时发送 <code>Content-Type</code> 告诉服务端这次请求的 body 是什么格式的。Web 开发中数据传递最常用的两类格式分别是 JSON 和 Form。</p>
<p>框架内置了 <a href="https://github.com/koajs/bodyparser" target="_blank" rel="noopener">bodyParser</a> 中间件来对这两类格式的请求 body 解析成 object 挂载到 <code>ctx.request.body</code> 上。HTTP 协议中并不建议在通过 GET、HEAD 方法访问时传递 body，所以我们无法在 GET、HEAD 方法中按照此方法获取到内容。</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="comment">// POST /api/posts HTTP/1.1</span></span><br><span class="line"><span class="comment">// Host: localhost:3000</span></span><br><span class="line"><span class="comment">// Content-Type: application/json; charset=UTF-8</span></span><br><span class="line"><span class="comment">//</span></span><br><span class="line"><span class="comment">// &#123;"title": "controller", "content": "what is controller"&#125;</span></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">PostController</span> <span class="keyword">extends</span> <span class="title">Controller</span> </span>&#123;</span><br><span class="line">  <span class="keyword">async</span> listPosts() &#123;</span><br><span class="line">    assert.equal(<span class="keyword">this</span>.ctx.request.body.title, <span class="string">'controller'</span>);</span><br><span class="line">    assert.equal(<span class="keyword">this</span>.ctx.request.body.content, <span class="string">'what is controller'</span>);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>框架对 bodyParser 设置了一些默认参数，配置好之后拥有以下特性：</p>
<ul>
<li>当请求的 Content-Type 为 <code>application/json</code>，<code>application/json-patch+json</code>，<code>application/vnd.api+json</code> 和 <code>application/csp-report</code> 时，会按照 json 格式对请求 body 进行解析，并限制 body 最大长度为 <code>100kb</code>。</li>
<li>当请求的 Content-Type 为 <code>application/x-www-form-urlencoded</code> 时，会按照 form 格式对请求 body 进行解析，并限制 body 最大长度为 <code>100kb</code>。</li>
<li>如果解析成功，body 一定会是一个 Object（可能是一个数组）。</li>
</ul>
<p>一般来说我们最经常调整的配置项就是变更解析时允许的最大长度，可以在 <code>config/config.default.js</code> 中覆盖框架的默认值。</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="built_in">module</span>.exports = &#123;</span><br><span class="line">  bodyParser: &#123;</span><br><span class="line">    jsonLimit: <span class="string">'1mb'</span>,</span><br><span class="line">    formLimit: <span class="string">'1mb'</span>,</span><br><span class="line">  &#125;,</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>
<p>如果用户的请求 body 超过了我们配置的解析最大长度，会抛出一个状态码为 <code>413</code> 的异常，如果用户请求的 body 解析失败（错误的 JSON），会抛出一个状态码为 <code>400</code> 的异常。</p>
<p><strong>注意：在调整 bodyParser 支持的 body 长度时，如果我们应用前面还有一层反向代理（Nginx），可能也需要调整它的配置，确保反向代理也支持同样长度的请求 body。</strong></p>
<p><strong>一个常见的错误是把 <code>ctx.request.body</code> 和 <code>ctx.body</code> 混淆，后者其实是 <code>ctx.response.body</code> 的简写。</strong></p>
<h3 id="获取上传的文件"><a class="markdown-anchor" href="#获取上传的文件">#</a> 获取上传的文件</h3>
<p>请求 body 除了可以带参数之外，还可以发送文件，一般来说，浏览器上都是通过 <code>Multipart/form-data</code> 格式发送文件的，框架通过内置 <a href="https://github.com/eggjs/egg-multipart" target="_blank" rel="noopener">Multipart</a> 插件来支持获取用户上传的文件。</p>
<p>完整的上传示例参见：<a href="https://github.com/eggjs/examples/tree/master/multipart" target="_blank" rel="noopener">eggjs/examples/multipart</a>。</p>
<p>在 Controller 中，我们可以通过 <code>ctx.getFileStream()</code> 接口能获取到上传的文件流。</p>
<figure class="highlight html"><table><tr><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">form</span> <span class="attr">method</span>=<span class="string">"POST"</span> <span class="attr">action</span>=<span class="string">"/upload?_csrf=&#123;&#123; ctx.csrf | safe &#125;&#125;"</span> <span class="attr">enctype</span>=<span class="string">"multipart/form-data"</span>&gt;</span></span><br><span class="line">  title: <span class="tag">&lt;<span class="name">input</span> <span class="attr">name</span>=<span class="string">"title"</span> /&gt;</span></span><br><span class="line">  file: <span class="tag">&lt;<span class="name">input</span> <span class="attr">name</span>=<span class="string">"file"</span> <span class="attr">type</span>=<span class="string">"file"</span> /&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">button</span> <span class="attr">type</span>=<span class="string">"submit"</span>&gt;</span>上传<span class="tag">&lt;/<span class="name">button</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">form</span>&gt;</span></span><br></pre></td></tr></table></figure>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="keyword">const</span> path = <span class="built_in">require</span>(<span class="string">'path'</span>);</span><br><span class="line"><span class="keyword">const</span> sendToWormhole = <span class="built_in">require</span>(<span class="string">'stream-wormhole'</span>);</span><br><span class="line"><span class="keyword">const</span> Controller = <span class="built_in">require</span>(<span class="string">'egg'</span>).Controller;</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">UploaderController</span> <span class="keyword">extends</span> <span class="title">Controller</span> </span>&#123;</span><br><span class="line">  <span class="keyword">async</span> upload() &#123;</span><br><span class="line">    <span class="keyword">const</span> ctx = <span class="keyword">this</span>.ctx;</span><br><span class="line">    <span class="keyword">const</span> stream = <span class="keyword">await</span> ctx.getFileStream();</span><br><span class="line">    <span class="keyword">const</span> name = <span class="string">'egg-multipart-test/'</span> + path.basename(stream.filename);</span><br><span class="line">    <span class="comment">// 文件处理，上传到云存储等等</span></span><br><span class="line">    <span class="keyword">let</span> result;</span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">      result = <span class="keyword">await</span> ctx.oss.put(name, stream);</span><br><span class="line">    &#125; <span class="keyword">catch</span> (err) &#123;</span><br><span class="line">      <span class="comment">// 必须将上传的文件流消费掉，要不然浏览器响应会卡死</span></span><br><span class="line">      <span class="keyword">await</span> sendToWormhole(stream);</span><br><span class="line">      <span class="keyword">throw</span> err;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    ctx.body = &#123;</span><br><span class="line">      url: result.url,</span><br><span class="line">      <span class="comment">// 所有表单字段都能通过 `stream.fields` 获取到</span></span><br><span class="line">      fields: stream.fields,</span><br><span class="line">    &#125;;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="built_in">module</span>.exports = UploaderController;</span><br></pre></td></tr></table></figure>
<p>要通过 <code>ctx.getFileStream</code> 便捷的获取到用户上传的文件，需要满足两个条件：</p>
<ul>
<li>只支持上传一个文件。</li>
<li>上传文件必须在所有其他的 fields 后面，否则在拿到文件流时可能还获取不到 fields。</li>
</ul>
<p>如果要获取同时上传的多个文件，不能通过 <code>ctx.getFileStream()</code> 来获取，只能通过下面这种方式：</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="keyword">const</span> sendToWormhole = <span class="built_in">require</span>(<span class="string">'stream-wormhole'</span>);</span><br><span class="line"><span class="keyword">const</span> Controller = <span class="built_in">require</span>(<span class="string">'egg'</span>).Controller;</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">UploaderController</span> <span class="keyword">extends</span> <span class="title">Controller</span> </span>&#123;</span><br><span class="line">  <span class="keyword">async</span> upload() &#123;</span><br><span class="line">    <span class="keyword">const</span> ctx = <span class="keyword">this</span>.ctx;</span><br><span class="line">    <span class="keyword">const</span> parts = ctx.multipart();</span><br><span class="line">    <span class="keyword">let</span> part;</span><br><span class="line">    <span class="comment">// parts() return a promise</span></span><br><span class="line">    <span class="keyword">while</span> ((part = <span class="keyword">await</span> parts()) != <span class="literal">null</span>) &#123;</span><br><span class="line">      <span class="keyword">if</span> (part.length) &#123;</span><br><span class="line">        <span class="comment">// 如果是数组的话是 filed</span></span><br><span class="line">        <span class="built_in">console</span>.log(<span class="string">'field: '</span> + part[<span class="number">0</span>]);</span><br><span class="line">        <span class="built_in">console</span>.log(<span class="string">'value: '</span> + part[<span class="number">1</span>]);</span><br><span class="line">        <span class="built_in">console</span>.log(<span class="string">'valueTruncated: '</span> + part[<span class="number">2</span>]);</span><br><span class="line">        <span class="built_in">console</span>.log(<span class="string">'fieldnameTruncated: '</span> + part[<span class="number">3</span>]);</span><br><span class="line">      &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        <span class="keyword">if</span> (!part.filename) &#123;</span><br><span class="line">          <span class="comment">// 这时是用户没有选择文件就点击了上传(part 是 file stream，但是 part.filename 为空)</span></span><br><span class="line">          <span class="comment">// 需要做出处理，例如给出错误提示消息</span></span><br><span class="line">          <span class="keyword">return</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">// part 是上传的文件流</span></span><br><span class="line">        <span class="built_in">console</span>.log(<span class="string">'field: '</span> + part.fieldname);</span><br><span class="line">        <span class="built_in">console</span>.log(<span class="string">'filename: '</span> + part.filename);</span><br><span class="line">        <span class="built_in">console</span>.log(<span class="string">'encoding: '</span> + part.encoding);</span><br><span class="line">        <span class="built_in">console</span>.log(<span class="string">'mime: '</span> + part.mime);</span><br><span class="line">        <span class="comment">// 文件处理，上传到云存储等等</span></span><br><span class="line">        <span class="keyword">let</span> result;</span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">          result = <span class="keyword">await</span> ctx.oss.put(<span class="string">'egg-multipart-test/'</span> + part.filename, part);</span><br><span class="line">        &#125; <span class="keyword">catch</span> (err) &#123;</span><br><span class="line">          <span class="comment">// 必须将上传的文件流消费掉，要不然浏览器响应会卡死</span></span><br><span class="line">          <span class="keyword">await</span> sendToWormhole(part);</span><br><span class="line">          <span class="keyword">throw</span> err;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="built_in">console</span>.log(result);</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="built_in">console</span>.log(<span class="string">'and we are done parsing the form!'</span>);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="built_in">module</span>.exports = UploaderController;</span><br></pre></td></tr></table></figure>
<p>为了保证文件上传的安全，框架限制了支持的的文件格式，框架默认支持白名单如下：</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="comment">// images</span></span><br><span class="line"><span class="string">'.jpg'</span>, <span class="string">'.jpeg'</span>, <span class="comment">// image/jpeg</span></span><br><span class="line"><span class="string">'.png'</span>, <span class="comment">// image/png, image/x-png</span></span><br><span class="line"><span class="string">'.gif'</span>, <span class="comment">// image/gif</span></span><br><span class="line"><span class="string">'.bmp'</span>, <span class="comment">// image/bmp</span></span><br><span class="line"><span class="string">'.wbmp'</span>, <span class="comment">// image/vnd.wap.wbmp</span></span><br><span class="line"><span class="string">'.webp'</span>,</span><br><span class="line"><span class="string">'.tif'</span>,</span><br><span class="line"><span class="string">'.psd'</span>,</span><br><span class="line"><span class="comment">// text</span></span><br><span class="line"><span class="string">'.svg'</span>,</span><br><span class="line"><span class="string">'.js'</span>, <span class="string">'.jsx'</span>,</span><br><span class="line"><span class="string">'.json'</span>,</span><br><span class="line"><span class="string">'.css'</span>, <span class="string">'.less'</span>,</span><br><span class="line"><span class="string">'.html'</span>, <span class="string">'.htm'</span>,</span><br><span class="line"><span class="string">'.xml'</span>,</span><br><span class="line"><span class="comment">// tar</span></span><br><span class="line"><span class="string">'.zip'</span>,</span><br><span class="line"><span class="string">'.gz'</span>, <span class="string">'.tgz'</span>, <span class="string">'.gzip'</span>,</span><br><span class="line"><span class="comment">// video</span></span><br><span class="line"><span class="string">'.mp3'</span>,</span><br><span class="line"><span class="string">'.mp4'</span>,</span><br><span class="line"><span class="string">'.avi'</span>,</span><br></pre></td></tr></table></figure>
<p>用户可以通过在 <code>config/config.default.js</code> 中配置来新增支持的文件扩展名，或者重写整个白名单</p>
<ul>
<li>新增支持的文件扩展名</li>
</ul>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="built_in">module</span>.exports = &#123;</span><br><span class="line">  multipart: &#123;</span><br><span class="line">    fileExtensions: [ <span class="string">'.apk'</span> ], <span class="comment">// 增加对 .apk 扩展名的支持</span></span><br><span class="line">  &#125;,</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>
<ul>
<li>覆盖整个白名单</li>
</ul>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="built_in">module</span>.exports = &#123;</span><br><span class="line">  multipart: &#123;</span><br><span class="line">    whitelist: [ <span class="string">'.png'</span> ], <span class="comment">// 覆盖整个白名单，只允许上传 '.png' 格式</span></span><br><span class="line">  &#125;,</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>
<p><strong>注意：当传递了 whitelist 属性时，fileExtensions 属性不生效。</strong></p>
<h3 id="header"><a class="markdown-anchor" href="#header">#</a> header</h3>
<p>除了从 URL 和请求 body 上获取参数之外，还有许多参数是通过请求 header 传递的。框架提供了一些辅助属性和方法来获取。</p>
<ul>
<li><code>ctx.headers</code>，<code>ctx.header</code>，<code>ctx.request.headers</code>，<code>ctx.request.header</code>：这几个方法是等价的，都是获取整个 header 对象。</li>
<li><code>ctx.get(name)</code>，<code>ctx.request.get(name)</code>：获取请求 header 中的一个字段的值，如果这个字段不存在，会返回空字符串。</li>
<li>我们建议用 <code>ctx.get(name)</code> 而不是 <code>ctx.headers['name']</code>，因为前者会自动处理大小写。</li>
</ul>
<p>由于 header 比较特殊，有一些是 <code>HTTP</code> 协议规定了具体含义的（例如 <code>Content-Type</code>，<code>Accept</code>），有些是反向代理设置的，已经约定俗成（X-Forwarded-For），框架也会对他们增加一些便捷的 getter，详细的 getter 可以查看 <a href="https://eggjs.org/api/">API</a> 文档。</p>
<p>特别是如果我们通过 <code>config.proxy = true</code> 设置了应用部署在反向代理（Nginx）之后，有一些 Getter 的内部处理会发生改变。</p>
<h4 id="ctxhost"><a class="markdown-anchor" href="#ctxhost">#</a> <code>ctx.host</code></h4>
<p>优先读通过 <code>config.hostHeaders</code> 中配置的 header 的值，读不到时再尝试获取 host 这个 header 的值，如果都获取不到，返回空字符串。</p>
<p><code>config.hostHeaders</code> 默认配置为 <code>x-forwarded-host</code>。</p>
<h4 id="ctxprotocol"><a class="markdown-anchor" href="#ctxprotocol">#</a> <code>ctx.protocol</code></h4>
<p>通过这个 Getter 获取 protocol 时，首先会判断当前连接是否是加密连接，如果是加密连接，返回 https。</p>
<p>如果处于非加密连接时，优先读通过 <code>config.protocolHeaders</code> 中配置的 header 的值来判断是 HTTP 还是 https，如果读取不到，我们可以在配置中通过 <code>config.protocol</code> 来设置兜底值，默认为 HTTP。</p>
<p><code>config.protocolHeaders</code> 默认配置为 <code>x-forwarded-proto</code>。</p>
<h4 id="ctxips"><a class="markdown-anchor" href="#ctxips">#</a> <code>ctx.ips</code></h4>
<p>通过 <code>ctx.ips</code> 获取请求经过所有的中间设备 IP 地址列表，只有在 <code>config.proxy = true</code> 时，才会通过读取 <code>config.ipHeaders</code> 中配置的 header 的值来获取，获取不到时为空数组。</p>
<p><code>config.ipHeaders</code> 默认配置为 <code>x-forwarded-for</code>。</p>
<h4 id="ctxip"><a class="markdown-anchor" href="#ctxip">#</a> <code>ctx.ip</code></h4>
<p>通过 <code>ctx.ip</code> 获取请求发起方的 IP 地址，优先从 <code>ctx.ips</code> 中获取，<code>ctx.ips</code> 为空时使用连接上发起方的 IP 地址。</p>
<p><strong>注意：<code>ip</code> 和 <code>ips</code> 不同，<code>ip</code> 当 <code>config.proxy = false</code> 时会返回当前连接发起者的 <code>ip</code> 地址，<code>ips</code> 此时会为空数组。</strong></p>
<h3 id="cookie"><a class="markdown-anchor" href="#cookie">#</a> Cookie</h3>
<p>HTTP 请求都是无状态的，但是我们的 Web 应用通常都需要知道发起请求的人是谁。为了解决这个问题，HTTP 协议设计了一个特殊的请求头：<a href="https://en.wikipedia.org/wiki/HTTP_cookie" target="_blank" rel="noopener">Cookie</a>。服务端可以通过响应头（set-cookie）将少量数据响应给客户端，浏览器会遵循协议将数据保存，并在下次请求同一个服务的时候带上（浏览器也会遵循协议，只在访问符合 Cookie 指定规则的网站时带上对应的 Cookie 来保证安全性）。</p>
<p>通过 <code>ctx.cookies</code>，我们可以在 Controller 中便捷、安全的设置和读取 Cookie。</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">CookieController</span> <span class="keyword">extends</span> <span class="title">Controller</span> </span>&#123;</span><br><span class="line">  <span class="keyword">async</span> add() &#123;</span><br><span class="line">    <span class="keyword">const</span> ctx = <span class="keyword">this</span>.ctx;</span><br><span class="line">    <span class="keyword">const</span> count = ctx.cookies.get(<span class="string">'count'</span>);</span><br><span class="line">    count = count ? <span class="built_in">Number</span>(count) : <span class="number">0</span>;</span><br><span class="line">    ctx.cookies.set(<span class="string">'count'</span>, ++count);</span><br><span class="line">    ctx.body = count;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">async</span> remove() &#123;</span><br><span class="line">    <span class="keyword">const</span> ctx = <span class="keyword">this</span>.ctx;</span><br><span class="line">    <span class="keyword">const</span> count = ctx.cookies.set(<span class="string">'count'</span>, <span class="literal">null</span>);</span><br><span class="line">    ctx.status = <span class="number">204</span>;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>Cookie 虽然在 HTTP 中只是一个头，但是通过 <code>foo=bar;foo1=bar1;</code> 的格式可以设置多个键值对。</p>
<p>Cookie 在 Web 应用中经常承担了传递客户端身份信息的作用，因此有许多安全相关的配置，不可忽视，<a href="../core/cookie-and-session.html#cookie">Cookie</a> 文档中详细介绍了 Cookie 的用法和安全相关的配置项，可以深入阅读了解。</p>
<h3 id="session"><a class="markdown-anchor" href="#session">#</a> Session</h3>
<p>通过 Cookie，我们可以给每一个用户设置一个 Session，用来存储用户身份相关的信息，这份信息会加密后存储在 Cookie 中，实现跨请求的用户身份保持。</p>
<p>框架内置了 <a href="https://github.com/eggjs/egg-session" target="_blank" rel="noopener">Session</a> 插件，给我们提供了 <code>ctx.session</code> 来访问或者修改当前用户 Session 。</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">PostController</span> <span class="keyword">extends</span> <span class="title">Controller</span> </span>&#123;</span><br><span class="line">  <span class="keyword">async</span> fetchPosts() &#123;</span><br><span class="line">    <span class="keyword">const</span> ctx = <span class="keyword">this</span>.ctx;</span><br><span class="line">    <span class="comment">// 获取 Session 上的内容</span></span><br><span class="line">    <span class="keyword">const</span> userId = ctx.session.userId;</span><br><span class="line">    <span class="keyword">const</span> posts = <span class="keyword">await</span> ctx.service.post.fetch(userId);</span><br><span class="line">    <span class="comment">// 修改 Session 的值</span></span><br><span class="line">    ctx.session.visited = ctx.session.visited ? ++ctx.session.visited : <span class="number">1</span>;</span><br><span class="line">    ctx.body = &#123;</span><br><span class="line">      success: <span class="literal">true</span>,</span><br><span class="line">      posts,</span><br><span class="line">    &#125;;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>Session 的使用方法非常直观，直接读取它或者修改它就可以了，如果要删除它，直接将它赋值为 <code>null</code>：</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">SessionController</span> <span class="keyword">extends</span> <span class="title">Controller</span> </span>&#123;</span><br><span class="line">  <span class="keyword">async</span> deleteSession() &#123;</span><br><span class="line">    <span class="keyword">this</span>.ctx.session = <span class="literal">null</span>;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>
<p>和 Cookie 一样，Session 也有许多安全等选项和功能，在使用之前也最好阅读 <a href="../core/cookie-and-session.html#session">Session</a> 文档深入了解。</p>
<h4 id="配置"><a class="markdown-anchor" href="#配置">#</a> 配置</h4>
<p>对于 Session 来说，主要有下面几个属性可以在 <code>config.default.js</code> 中进行配置:</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="built_in">module</span>.exports = &#123;</span><br><span class="line">  key: <span class="string">'EGG_SESS'</span>, <span class="comment">// 承载 Session 的 Cookie 键值对名字</span></span><br><span class="line">  maxAge: <span class="number">86400000</span>, <span class="comment">// Session 的最大有效时间</span></span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>
<h2 id="参数校验"><a class="markdown-anchor" href="#参数校验">#</a> 参数校验</h2>
<p>在获取到用户请求的参数后，不可避免的要对参数进行一些校验。</p>
<p>借助 <a href="https://github.com/eggjs/egg-validate" target="_blank" rel="noopener">Validate</a> 插件提供便捷的参数校验机制，帮助我们完成各种复杂的参数校验。</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="comment">// config/plugin.js</span></span><br><span class="line">exports.validate = &#123;</span><br><span class="line">  enable: <span class="literal">true</span>,</span><br><span class="line">  package: <span class="string">'egg-validate'</span>,</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>
<p>通过 <code>ctx.validate(rule, [body])</code> 直接对参数进行校验：</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">PostController</span> <span class="keyword">extends</span> <span class="title">Controller</span> </span>&#123;</span><br><span class="line">  <span class="keyword">async</span> create() &#123;</span><br><span class="line">    <span class="comment">// 校验参数</span></span><br><span class="line">    <span class="comment">// 如果不传第二个参数会自动校验 `ctx.request.body`</span></span><br><span class="line">    <span class="keyword">this</span>.ctx.validate(&#123;</span><br><span class="line">      title: &#123; <span class="attr">type</span>: <span class="string">'string'</span> &#125;,</span><br><span class="line">      content: &#123; <span class="attr">type</span>: <span class="string">'string'</span> &#125;,</span><br><span class="line">    &#125;);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>当校验异常时，会直接抛出一个异常，异常的状态码为 422，errors 字段包含了详细的验证不通过信息。如果想要自己处理检查的异常，可以通过 <code>try catch</code> 来自行捕获。</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">PostController</span> <span class="keyword">extends</span> <span class="title">Controller</span> </span>&#123;</span><br><span class="line">  <span class="keyword">async</span> create() &#123;</span><br><span class="line">    <span class="keyword">const</span> ctx = <span class="keyword">this</span>.ctx;</span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">      ctx.validate(createRule);</span><br><span class="line">    &#125; <span class="keyword">catch</span> (err) &#123;</span><br><span class="line">      ctx.logger.warn(err.errors);</span><br><span class="line">      ctx.body = &#123; <span class="attr">success</span>: <span class="literal">false</span> &#125;;</span><br><span class="line">      <span class="keyword">return</span>;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>
<h3 id="校验规则"><a class="markdown-anchor" href="#校验规则">#</a> 校验规则</h3>
<p>参数校验通过 <a href="https://github.com/node-modules/parameter#rule" target="_blank" rel="noopener">Parameter</a> 完成，支持的校验规则可以在该模块的文档中查阅到。</p>
<h4 id="自定义校验规则"><a class="markdown-anchor" href="#自定义校验规则">#</a> 自定义校验规则</h4>
<p>除了上一节介绍的内置检验类型外，有时候我们希望自定义一些校验规则，让开发时更便捷，此时可以通过 <code>app.validator.addRule(type, check)</code> 的方式新增自定义规则。</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="comment">// app.js</span></span><br><span class="line">app.validator.addRule(<span class="string">'json'</span>, (rule, value) =&gt; &#123;</span><br><span class="line">  <span class="keyword">try</span> &#123;</span><br><span class="line">    <span class="built_in">JSON</span>.parse(value);</span><br><span class="line">  &#125; <span class="keyword">catch</span> (err) &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="string">'must be json string'</span>;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>
<p>添加完自定义规则之后，就可以在 Controller 中直接使用这条规则来进行参数校验了</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">PostController</span> <span class="keyword">extends</span> <span class="title">Controller</span> </span>&#123;</span><br><span class="line">  <span class="keyword">async</span> handler() &#123;</span><br><span class="line">    <span class="keyword">const</span> ctx = <span class="keyword">this</span>.ctx;</span><br><span class="line">    <span class="comment">// query.test 字段必须是 json 字符串</span></span><br><span class="line">    <span class="keyword">const</span> rule = &#123; <span class="attr">test</span>: <span class="string">'json'</span> &#125;;</span><br><span class="line">    ctx.validate(rule, ctx.query);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>
<h2 id="调用-service"><a class="markdown-anchor" href="#调用-service">#</a> 调用 Service</h2>
<p>我们并不想在 Controller 中实现太多业务逻辑，所以提供了一个 <a href="./service.html">Service</a> 层进行业务逻辑的封装，这不仅能提高代码的复用性，同时可以让我们的业务逻辑更好测试。</p>
<p>在 Controller 中可以调用任何一个 Service 上的任何方法，同时 Service 是懒加载的，只有当访问到它的时候框架才会去实例化它。</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">PostController</span> <span class="keyword">extends</span> <span class="title">Controller</span> </span>&#123;</span><br><span class="line">  <span class="keyword">async</span> create() &#123;</span><br><span class="line">    <span class="keyword">const</span> ctx = <span class="keyword">this</span>.ctx;</span><br><span class="line">    <span class="keyword">const</span> author = ctx.session.userId;</span><br><span class="line">    <span class="keyword">const</span> req = <span class="built_in">Object</span>.assign(ctx.request.body, &#123; author &#125;);</span><br><span class="line">    <span class="comment">// 调用 service 进行业务处理</span></span><br><span class="line">    <span class="keyword">const</span> res = <span class="keyword">await</span> ctx.service.post.create(req);</span><br><span class="line">    ctx.body = &#123; <span class="attr">id</span>: res.id &#125;;</span><br><span class="line">    ctx.status = <span class="number">201</span>;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>Service 的具体写法，请查看 <a href="./service.html">Service</a> 章节。</p>
<h2 id="发送-http-响应"><a class="markdown-anchor" href="#发送-http-响应">#</a> 发送 HTTP 响应</h2>
<p>当业务逻辑完成之后，Controller 的最后一个职责就是将业务逻辑的处理结果通过 HTTP 响应发送给用户。</p>
<h3 id="设置-status"><a class="markdown-anchor" href="#设置-status">#</a> 设置 status</h3>
<p>HTTP 设计了非常多的<a href="https://en.wikipedia.org/wiki/List_of_HTTP_status_codes" target="_blank" rel="noopener">状态码</a>，每一个状态码都代表了一个特定的含义，通过设置正确的状态码，可以让响应更符合语义。</p>
<p>框架提供了一个便捷的 Setter 来进行状态码的设置</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">PostController</span> <span class="keyword">extends</span> <span class="title">Controller</span> </span>&#123;</span><br><span class="line">  <span class="keyword">async</span> create() &#123;</span><br><span class="line">    <span class="comment">// 设置状态码为 201</span></span><br><span class="line">    <span class="keyword">this</span>.ctx.status = <span class="number">201</span>;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>
<p>具体什么场景设置什么样的状态码，可以参考 <a href="https://en.wikipedia.org/wiki/List_of_HTTP_status_codes" target="_blank" rel="noopener">List of HTTP status codes</a> 中各个状态码的含义。</p>
<h3 id="设置-body"><a class="markdown-anchor" href="#设置-body">#</a> 设置 body</h3>
<p>绝大多数的数据都是通过 body 发送给请求方的，和请求中的 body 一样，在响应中发送的 body，也需要有配套的 Content-Type 告知客户端如何对数据进行解析。</p>
<ul>
<li>作为一个 RESTful 的 API 接口 controller，我们通常会返回 Content-Type 为 <code>application/json</code> 格式的 body，内容是一个 JSON 字符串。</li>
<li>作为一个 html 页面的 controller，我们通常会返回 Content-Type 为 <code>text/html</code> 格式的 body，内容是 html 代码段。</li>
</ul>
<p><strong>注意：<code>ctx.body</code> 是 <code>ctx.response.body</code> 的简写，不要和 <code>ctx.request.body</code> 混淆了。</strong></p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">ViewController</span> <span class="keyword">extends</span> <span class="title">Controller</span> </span>&#123;</span><br><span class="line">  <span class="keyword">async</span> show() &#123;</span><br><span class="line">    <span class="keyword">this</span>.ctx.body = &#123;</span><br><span class="line">      name: <span class="string">'egg'</span>,</span><br><span class="line">      category: <span class="string">'framework'</span>,</span><br><span class="line">      language: <span class="string">'Node.js'</span>,</span><br><span class="line">    &#125;;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">async</span> page() &#123;</span><br><span class="line">    <span class="keyword">this</span>.ctx.body = <span class="string">'&lt;html&gt;&lt;h1&gt;Hello&lt;/h1&gt;&lt;/html&gt;'</span>;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>由于 Node.js 的流式特性，我们还有很多场景需要通过 Stream 返回响应，例如返回一个大文件，代理服务器直接返回上游的内容，框架也支持直接将 body 设置成一个 Stream，并会同时处理好这个 Stream 上的错误事件。</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">ProxyController</span> <span class="keyword">extends</span> <span class="title">Controller</span> </span>&#123;</span><br><span class="line">  <span class="keyword">async</span> proxy() &#123;</span><br><span class="line">    <span class="keyword">const</span> ctx = <span class="keyword">this</span>.ctx;</span><br><span class="line">    <span class="keyword">const</span> result = <span class="keyword">await</span> ctx.curl(url, &#123;</span><br><span class="line">      streaming: <span class="literal">true</span>,</span><br><span class="line">    &#125;);</span><br><span class="line">    ctx.set(result.header);</span><br><span class="line">    <span class="comment">// result.res 是一个 stream</span></span><br><span class="line">    ctx.body = result.res;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>
<h4 id="渲染模板"><a class="markdown-anchor" href="#渲染模板">#</a> 渲染模板</h4>
<p>通常来说，我们不会手写 HTML 页面，而是会通过模板引擎进行生成。
框架自身没有集成任何一个模板引擎，但是约定了 <a href="../advanced/view-plugin.html">View 插件的规范</a>，通过接入的模板引擎，可以直接使用 <code>ctx.render(template)</code> 来渲染模板生成 html。</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">HomeController</span> <span class="keyword">extends</span> <span class="title">Controller</span> </span>&#123;</span><br><span class="line">  <span class="keyword">async</span> index() &#123;</span><br><span class="line">    <span class="keyword">const</span> ctx = <span class="keyword">this</span>.ctx;</span><br><span class="line">    <span class="keyword">await</span> ctx.render(<span class="string">'home.tpl'</span>, &#123; <span class="attr">name</span>: <span class="string">'egg'</span> &#125;);</span><br><span class="line">    <span class="comment">// ctx.body = await ctx.renderString('hi, &#123;&#123; name &#125;&#125;', &#123; name: 'egg' &#125;);</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>
<p>具体示例可以查看<a href="../core/view.html">模板渲染</a>。</p>
<h4 id="jsonp"><a class="markdown-anchor" href="#jsonp">#</a> JSONP</h4>
<p>有时我们需要给非本域的页面提供接口服务，又由于一些历史原因无法通过 <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS" target="_blank" rel="noopener">CORS</a> 实现，可以通过 <a href="https://en.wikipedia.org/wiki/JSONP" target="_blank" rel="noopener">JSONP</a> 来进行响应。</p>
<p>由于 JSONP 如果使用不当会导致非常多的安全问题，所以框架中提供了便捷的响应 JSONP 格式数据的方法，封装了 <a href="../core/security.html#jsonp-xss">JSONP XSS 相关的安全防范</a>，并支持进行 CSRF 校验和 referrer 校验。</p>
<ul>
<li>通过 <code>app.jsonp()</code> 提供的中间件来让一个 controller 支持响应 JSONP 格式的数据。在路由中，我们给需要支持 jsonp 的路由加上这个中间件：</li>
</ul>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="comment">// app/router.js</span></span><br><span class="line"><span class="built_in">module</span>.exports = <span class="function"><span class="params">app</span> =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">const</span> jsonp = app.jsonp();</span><br><span class="line">  app.router.get(<span class="string">'/api/posts/:id'</span>, jsonp, app.controller.posts.show);</span><br><span class="line">  app.router.get(<span class="string">'/api/posts'</span>, jsonp, app.controller.posts.list);</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>
<ul>
<li>在 Controller 中，只需要正常编写即可：</li>
</ul>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="comment">// app/controller/posts.js</span></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">PostController</span> <span class="keyword">extends</span> <span class="title">Controller</span> </span>&#123;</span><br><span class="line">  <span class="keyword">async</span> show() &#123;</span><br><span class="line">    <span class="keyword">this</span>.ctx.body = &#123;</span><br><span class="line">      name: <span class="string">'egg'</span>,</span><br><span class="line">      category: <span class="string">'framework'</span>,</span><br><span class="line">      language: <span class="string">'Node.js'</span>,</span><br><span class="line">    &#125;;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>用户请求对应的 URL 访问到这个 controller 的时候，如果 query 中有 <code>_callback=fn</code> 参数，将会返回 JSONP 格式的数据，否则返回 JSON 格式的数据。</p>
<h5 id="jsonp-配置"><a class="markdown-anchor" href="#jsonp-配置">#</a> JSONP 配置</h5>
<p>框架默认通过 query 中的 <code>_callback</code> 参数作为识别是否返回 JSONP 格式数据的依据，并且 <code>_callback</code> 中设置的方法名长度最多只允许 50 个字符。应用可以在 <code>config/config.default.js</code> 全局覆盖默认的配置：</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="comment">// config/config.default.js</span></span><br><span class="line">exports.jsonp = &#123;</span><br><span class="line">  callback: <span class="string">'callback'</span>, <span class="comment">// 识别 query 中的 `callback` 参数</span></span><br><span class="line">  limit: <span class="number">100</span>, <span class="comment">// 函数名最长为 100 个字符</span></span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>
<p>通过上面的方式配置之后，如果用户请求 <code>/api/posts/1?callback=fn</code>，响应为 JSONP 格式，如果用户请求 <code>/api/posts/1</code>，响应格式为 JSON。</p>
<p>我们同样可以在 <code>app.jsonp()</code> 创建中间件时覆盖默认的配置，以达到不同路由使用不同配置的目的：</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="comment">// app/router.js</span></span><br><span class="line"><span class="built_in">module</span>.exports = <span class="function"><span class="params">app</span> =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">const</span> &#123; router, controller, jsonp &#125; = app;</span><br><span class="line">  router.get(<span class="string">'/api/posts/:id'</span>, jsonp(&#123; <span class="attr">callback</span>: <span class="string">'callback'</span> &#125;), controller.posts.show);</span><br><span class="line">  router.get(<span class="string">'/api/posts'</span>, jsonp(&#123; <span class="attr">callback</span>: <span class="string">'cb'</span> &#125;), controller.posts.list);</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>
<h5 id="跨站防御配置"><a class="markdown-anchor" href="#跨站防御配置">#</a> 跨站防御配置</h5>
<p>默认配置下，响应 JSONP 时不会进行任何跨站攻击的防范，在某些情况下，这是很危险的。我们初略将 JSONP 接口分为三种类型：</p>
<ol>
<li>查询非敏感数据，例如获取一个论坛的公开文章列表。</li>
<li>查询敏感数据，例如获取一个用户的交易记录。</li>
<li>提交数据并修改数据库，例如给某一个用户创建一笔订单。</li>
</ol>
<p>如果我们的 JSONP 接口提供下面两类服务，在不做任何跨站防御的情况下，可能泄露用户敏感数据甚至导致用户被钓鱼。因此框架给 JSONP 默认提供了 CSRF 校验支持和 referrer 校验支持。</p>
<h6 id="csrf"><a class="markdown-anchor" href="#csrf">#</a> CSRF</h6>
<p>在 JSONP 配置中，我们只需要打开 <code>csrf: true</code>，即可对 JSONP 接口开启 CSRF 校验。</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="comment">// config/config.default.js</span></span><br><span class="line"><span class="built_in">module</span>.exports = &#123;</span><br><span class="line">  jsonp: &#123;</span><br><span class="line">    csrf: <span class="literal">true</span>,</span><br><span class="line">  &#125;,</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>
<p><strong>注意，CSRF 校验依赖于 <a href="../core/security.html">security</a> 插件提供的基于 Cookie 的 CSRF 校验。</strong></p>
<p>在开启 CSRF 校验时，客户端在发起 JSONP 请求时，也要带上 CSRF token，如果发起 JSONP 的请求方所在的页面和我们的服务在同一个主域名之下的话，可以读取到 Cookie 中的 CSRF token（在 CSRF token 缺失时也可以自行设置 CSRF token 到 Cookie 中），并在请求时带上该 token。</p>
<h5 id="referrer-校验"><a class="markdown-anchor" href="#referrer-校验">#</a> referrer 校验</h5>
<p>如果在同一个主域之下，可以通过开启 CSRF 的方式来校验 JSONP 请求的来源，而如果想对其他域名的网页提供 JSONP 服务，我们可以通过配置 referrer 白名单的方式来限制 JSONP 的请求方在可控范围之内。</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="comment">//config/config.default.js</span></span><br><span class="line">exports.jsonp = &#123;</span><br><span class="line">  whiteList: <span class="regexp">/^https?:\/\/test.com\//</span>,</span><br><span class="line">  <span class="comment">// whiteList: '.test.com',</span></span><br><span class="line">  <span class="comment">// whiteList: 'sub.test.com',</span></span><br><span class="line">  <span class="comment">// whiteList: [ 'sub.test.com', 'sub2.test.com' ],</span></span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>
<p><code>whiteList</code> 可以配置为正则表达式、字符串或者数组：</p>
<ul>
<li>正则表达式：此时只有请求的 Referrer 匹配该正则时才允许访问 JSONP 接口。在设置正则表达式的时候，注意开头的 <code>^</code> 以及结尾的 <code>\/</code>，保证匹配到完整的域名。</li>
</ul>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line">exports.jsonp = &#123;</span><br><span class="line">  whiteList: <span class="regexp">/^https?:\/\/test.com\//</span>,</span><br><span class="line">&#125;;</span><br><span class="line"><span class="comment">// matches referrer:</span></span><br><span class="line"><span class="comment">// https://test.com/hello</span></span><br><span class="line"><span class="comment">// http://test.com/</span></span><br></pre></td></tr></table></figure>
<ul>
<li>字符串：设置字符串形式的白名单时分为两种，当字符串以 <code>.</code> 开头，例如 <code>.test.com</code> 时，代表 referrer 白名单为 <code>test.com</code> 的所有子域名，包括 <code>test.com</code> 自身。当字符串不以 <code>.</code> 开头，例如 <code>sub.test.com</code>，代表 referrer 白名单为 <code>sub.test.com</code> 这一个域名。（同时支持 HTTP 和 HTTPS）。</li>
</ul>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line">exports.jsonp = &#123;</span><br><span class="line">  whiteList: <span class="string">'.test.com'</span>,</span><br><span class="line">&#125;;</span><br><span class="line"><span class="comment">// matches domain test.com:</span></span><br><span class="line"><span class="comment">// https://test.com/hello</span></span><br><span class="line"><span class="comment">// http://test.com/</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// matches subdomain</span></span><br><span class="line"><span class="comment">// https://sub.test.com/hello</span></span><br><span class="line"><span class="comment">// http://sub.sub.test.com/</span></span><br><span class="line"></span><br><span class="line">exports.jsonp = &#123;</span><br><span class="line">  whiteList: <span class="string">'sub.test.com'</span>,</span><br><span class="line">&#125;;</span><br><span class="line"><span class="comment">// only matches domain sub.test.com:</span></span><br><span class="line"><span class="comment">// https://sub.test.com/hello</span></span><br><span class="line"><span class="comment">// http://sub.test.com/</span></span><br></pre></td></tr></table></figure>
<ul>
<li>数组：当设置的白名单为数组时，代表只要满足数组中任意一个元素的条件即可通过 referrer 校验。</li>
</ul>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line">exports.jsonp = &#123;</span><br><span class="line">  whiteList: [ <span class="string">'sub.test.com'</span>, <span class="string">'sub2.test.com'</span> ],</span><br><span class="line">&#125;;</span><br><span class="line"><span class="comment">// matches domain sub.test.com and sub2.test.com:</span></span><br><span class="line"><span class="comment">// https://sub.test.com/hello</span></span><br><span class="line"><span class="comment">// http://sub2.test.com/</span></span><br></pre></td></tr></table></figure>
<p><strong>当 CSRF 和 referrer 校验同时开启时，请求发起方只需要满足任意一个条件即可通过 JSONP 的安全校验。</strong></p>
<h3 id="设置-header"><a class="markdown-anchor" href="#设置-header">#</a> 设置 Header</h3>
<p>我们通过状态码标识请求成功与否、状态如何，在 body 中设置响应的内容。而通过响应的 Header，还可以设置一些扩展信息。</p>
<p>通过 <code>ctx.set(key, value)</code> 方法可以设置一个响应头，<code>ctx.set(headers)</code> 设置多个 Header。</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="comment">// app/controller/api.js</span></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">ProxyController</span> <span class="keyword">extends</span> <span class="title">Controller</span> </span>&#123;</span><br><span class="line">  <span class="keyword">async</span> show() &#123;</span><br><span class="line">    <span class="keyword">const</span> ctx = <span class="keyword">this</span>.ctx;</span><br><span class="line">    <span class="keyword">const</span> start = <span class="built_in">Date</span>.now();</span><br><span class="line">    ctx.body = <span class="keyword">await</span> ctx.service.post.get();</span><br><span class="line">    <span class="keyword">const</span> used = <span class="built_in">Date</span>.now() - start;</span><br><span class="line">    <span class="comment">// 设置一个响应头</span></span><br><span class="line">    ctx.set(<span class="string">'show-response-time'</span>, used.toString());</span><br><span class="line">  &#125;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>
<h3 id="重定向"><a class="markdown-anchor" href="#重定向">#</a> 重定向</h3>
<p>框架通过 security 插件覆盖了 koa 原生的 <code>ctx.redirect</code> 实现，以提供更加安全的重定向。</p>
<ul>
<li><code>ctx.redirect(url)</code> 如果不在配置的白名单域名内，则禁止跳转。</li>
<li><code>ctx.unsafeRedirect(url)</code> 不判断域名，直接跳转，一般不建议使用，明确了解可能带来的风险后使用。</li>
</ul>
<p>用户如果使用<code>ctx.redirect</code>方法，需要在应用的配置文件中做如下配置：</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="comment">// config/config.default.js</span></span><br><span class="line">exports.security = &#123;</span><br><span class="line">  domainWhiteList:[<span class="string">'.domain.com'</span>],  <span class="comment">// 安全白名单，以 . 开头</span></span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>
<p>若用户没有配置 <code>domainWhiteList</code> 或者 <code>domainWhiteList</code>数组内为空，则默认会对所有跳转请求放行，即等同于<code>ctx.unsafeRedirect(url)</code></p>

  </article>
  <aside id="mobileAside" class="toc">
  <div class="mobile-menu">
    <ul>
      <li><a href="/zh-cn/intro/" alt="指南">指南</a></li><li><a href="/api/" alt="API">API</a></li><li><a href="/zh-cn/tutorials/index.html" alt="教程">教程</a></li><li><a href="https://github.com/search?q=topic%3Aegg-plugin&type=Repositories" alt="插件">插件</a></li><li><a href="https://github.com/eggjs/egg/releases" alt="发布日志">发布日志</a></li>
      
      
        <li class="translations">
          <a class="nav-link">Translations</a>
          <span class="arrow"></span><ul id="dropdownContent" class="dropdown-content"><li><a id="en" href="/en/basics/controller.html" >English</a></li><li><a id="zh-cn" href="/zh-cn/basics/controller.html" style="color: #22ab28">中文</a></li></ul>
        </li>
      
    </ul>
  </div>
  <dl><dt id="title-Intro" style="cursor: pointer;" class="aside-title">新手指南<a id="collapse-icon-Intro" class="icon opend"></a></dt><dd id=panel-Intro><ul><li><a href="/zh-cn/intro/index.html" class="menu-link">Egg.js 是什么?</a></li><li><a href="/zh-cn/intro/egg-and-koa.html" class="menu-link">Egg.js 和 Koa</a></li><li><a href="/zh-cn/intro/quickstart.html" class="menu-link">快速入门</a></li><li><a href="/zh-cn/tutorials/progressive.html" class="menu-link">渐进式开发</a></li><li><a href="/zh-cn/migration.html" class="menu-link">2.x 升级指南</a></li></ul></dd><dt id="title-Basics" style="cursor: pointer;" class="aside-title">基础功能<a id="collapse-icon-Basics" class="icon opend"></a></dt><dd id=panel-Basics><ul><li><a href="/zh-cn/basics/structure.html" class="menu-link">目录结构</a></li><li><a href="/zh-cn/basics/objects.html" class="menu-link">内置对象</a></li><li><a href="/zh-cn/basics/env.html" class="menu-link">运行环境</a></li><li><a href="/zh-cn/basics/config.html" class="menu-link">配置</a></li><li><a href="/zh-cn/basics/middleware.html" class="menu-link">中间件</a></li><li><a href="/zh-cn/basics/router.html" class="menu-link">Router</a></li><li><a href="/zh-cn/basics/controller.html" class="menu-link">Controller</a></li><li><a href="/zh-cn/basics/service.html" class="menu-link">Service</a></li><li><a href="/zh-cn/basics/plugin.html" class="menu-link">插件</a></li><li><a href="/zh-cn/basics/schedule.html" class="menu-link">定时任务</a></li><li><a href="/zh-cn/basics/extend.html" class="menu-link">框架扩展</a></li><li><a href="/zh-cn/basics/app-start.html" class="menu-link">启动自定义</a></li></ul></dd><dt id="title-Core" style="cursor: pointer;" class="aside-title">核心功能<a id="collapse-icon-Core" class="icon opend"></a></dt><dd id=panel-Core><ul><li><a href="/zh-cn/core/development.html" class="menu-link">本地开发</a></li><li><a href="/zh-cn/core/unittest.html" class="menu-link">单元测试</a></li><li><a href="/zh-cn/core/deployment.html" class="menu-link">应用部署</a></li><li><a href="/zh-cn/core/logger.html" class="menu-link">日志</a></li><li><a href="/zh-cn/core/httpclient.html" class="menu-link">HttpClient</a></li><li><a href="/zh-cn/core/cookie-and-session.html" class="menu-link">Cookie and Session</a></li><li><a href="/zh-cn/core/cluster-and-ipc.html" class="menu-link">多进程模型和进程间通讯</a></li><li><a href="/zh-cn/core/view.html" class="menu-link">模板渲染</a></li><li><a href="/zh-cn/core/error-handling.html" class="menu-link">异常处理</a></li><li><a href="/zh-cn/core/security.html" class="menu-link">安全</a></li><li><a href="/zh-cn/core/i18n.html" class="menu-link">国际化</a></li></ul></dd><dt id="title-Tutorials" style="cursor: pointer;" class="aside-title">教程<a id="collapse-icon-Tutorials" class="icon opend"></a></dt><dd id=panel-Tutorials><ul><li><a href="/zh-cn/tutorials/mysql.html" class="menu-link">MySQL</a></li><li><a href="/zh-cn/tutorials/restful.html" class="menu-link">RESTful API</a></li><li><a href="/zh-cn/tutorials/passport.html" class="menu-link">Passport 鉴权</a></li><li><a href="/zh-cn/tutorials/socketio.html" class="menu-link">Socket.IO</a></li><li><a href="/zh-cn/tutorials/assets.html" class="menu-link">静态资源</a></li><li><a href="/zh-cn/tutorials/typescript.html" class="menu-link">TypeScript</a></li></ul></dd><dt id="title-Advanced" style="cursor: pointer;" class="aside-title">进阶<a id="collapse-icon-Advanced" class="icon opend"></a></dt><dd id=panel-Advanced><ul><li><a href="/zh-cn/advanced/loader.html" class="menu-link">Loader</a></li><li><a href="/zh-cn/advanced/plugin.html" class="menu-link">插件开发</a></li><li><a href="/zh-cn/advanced/framework.html" class="menu-link">框架开发</a></li><li><a href="/zh-cn/advanced/cluster-client.html" class="menu-link">多进程研发模式增强</a></li><li><a href="/zh-cn/advanced/view-plugin.html" class="menu-link">模板插件开发规范</a></li><li><a href="/zh-cn/style-guide.html" class="menu-link">代码风格指南</a></li></ul></dd><dt id="title-Community" style="cursor: pointer;" class="aside-title">社区<a id="collapse-icon-Community" class="icon opend"></a></dt><dd id=panel-Community><ul><li><a href="/zh-cn/plugins/" class="menu-link">内置插件列表</a></li><li><a href="/zh-cn/contributing.html" class="menu-link">如何贡献</a></li><li><a href="/zh-cn/resource.html" class="menu-link">资源</a></li><li><a href="/zh-cn/faq.html" class="menu-link">常见问题</a></li></ul></dd></dl>
</aside>
<script>
var mobileTrigger = document.getElementById('mobileTrigger');
var mobileAside = document.getElementById('mobileAside');

var expandMenu = function(title) {
  // handle icon
  const collapseIcon = document.getElementById('collapse-icon-' + title);
  if (collapseIcon) {
    collapseIcon.className = 'icon opend';
  }
  // handle panelEle
  const panelEle = document.getElementById('panel-' + title);
  if (panelEle) {
    panelEle.className = '';
  }
}

var collapseMenu = function(title) {
  // handle icon
  const collapseIcon = document.getElementById('collapse-icon-' + title);
  if (collapseIcon) {
    collapseIcon.className = 'icon closed';
  }
  // handle panelEle
  const panelEle = document.getElementById('panel-' + title);
  if (panelEle) {
    panelEle.className = 'aside-panel-hidden';
  }
}

mobileAside.onclick = function(e) {
  const targetId = e.target.id;
  if (targetId && (targetId.indexOf('title-') > -1 || targetId.indexOf('collapse-icon-') > -1)) {
    const title = targetId.replace('title-', '').replace('collapse-icon-', '');
    try { 
      // the the browser may have no localStroage or JSON.parse may throw exception.
      const menuInfo = JSON.parse(window.localStorage.getItem('menuInfo'));
        
      // current menu status
      const curClosed = menuInfo[title] ? menuInfo[title].closed : false; // default false

      // change UI
      curClosed ? expandMenu(title) : collapseMenu(title);

      // save menuInfo to localStorage
      menuInfo[title] = { closed: !curClosed } // opposite
      window.localStorage.setItem('menuInfo', JSON.stringify(menuInfo));
    } catch (e) {}
  }
};

mobileTrigger.onclick = function(e) {
  e.preventDefault();
  if (mobileAside.className.indexOf('mobile-show') === -1) {
    mobileAside.className += ' mobile-show';
  } else {
    mobileAside.className = 'toc';
  }
};

(function() {
  // save data to localStorage because the page will refresh when user change the url.
  let menuInfo;
  try { 
    // the the browser may have no localStroage or JSON.parse may throw exception.
    menuInfo = JSON.parse(window.localStorage.getItem('menuInfo'));
    if (!menuInfo) {
      menuInfo = {};
      window.localStorage.setItem('menuInfo', JSON.stringify(menuInfo));
    }
  } catch (e) {
    menuInfo = {}; // default {}
  }

  for (const title in menuInfo) {
    if (menuInfo[title] && menuInfo[title].closed) { // menu in closed status.
      collapseMenu(title);
    } else {
      expandMenu(title);
    }
  }

  // highlight menu
  const pathname = window.location.pathname;
  const selector = `a[href="${pathname}"].menu-link,a[href="${pathname}index.html"].menu-link`;
  const menuItem = mobileAside.querySelector(selector);
  if (menuItem) { menuItem.className += ' highlight'; }
})();
</script>

</div>

  </div>
</body>
<script src="https://cdn.jsdelivr.net/docsearch.js/2/docsearch.min.js"></script>
<script>
docsearch({
  apiKey: '1561de31a86f79507ea00cdb54ce647c',
  indexName: 'eggjs',
  inputSelector: '#search-query',
});
</script>
<div class="cnzz">
<script src="https://s11.cnzz.com/z_stat.php?id=1261142226&web_id=1261142226" language="JavaScript"></script>
</div>

</html>
